跳到主要内容

2.2-面向对象编程

Create by fall on 08 Sep 2021 Recently revised in 12 Jan 2023

关键字

instanceof

通过该关键字可以判断是否是继承关系

Array instanceof Object // true
let person = function(){}
let no = new person()
no instanceof person//true

instanceOf 原理

function new_instance_of(leftVaule, rightVaule) { 
let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
while (true) {
if (leftVaule === null) {
return false;
}
if (leftVaule === rightProto) {
return true;
}
leftVaule = leftVaule.__proto__
}
}

其实 instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。

new

在 JS 中,函数本身就是构造函数,可以直接通过 new 进行声明构造函数。

new 的作用实际上就是把 function 转换为一个对象

自己生成一个 new

function myNew(fun,...args){
let obj = {}
const res = fun.call(obj,...args)
obj.__proto__ = fun.prototype
// 如果存在返回值
if(res){
// operate
}
return obj
}
function Person(name){
this.name = name
this.showName= ()=>{
console.log(name);
}
}
Person.prototype.say =function(){
console.log(this.name+'说:我没钱了');
}
const liu = myNew(Person,'狗剩')
liu.showName()
liu.say()

ES6 为 new 命令引入了一个 new.target 属性,这个属性一般用在构造函数中,返回 new 调用的那个构造函数。

如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,所以这个属性可以用来确定构造函数是怎么调用的,遇到箭头函数会抛错。

function fn(name) {
console.log('fn:',new.target)
}
fn('nanjiu') // undefined
new fn('nanjiu')
/*
fn: ƒ fn(name) {
console.log('fn:',new.target)
}
*/
let fn2 = (name) => {
console.log('fn2',new.target)
}
fn2('nan') // 报错 Uncaught SyntaxError: new.target expression is not allowed here

Class

ES6 实现的面向对象方式,Class 是 ES6 中创建类的语法糖

语法特性

可以用 class 关键字来定义一个类,类是对一类具有共同特征的事物的抽象,就比如可以把狗定义为一个类,狗有名字会叫也会跳;类是特殊的函数,就像函数定义的时候有函数声明和函数表达式一样,类的定义也有类声明和类表达式,不过类声明不同于函数声明,它是无法提升的;类也有 name 属性

// 类声明
class Dog {
constructor(name) { // 在创建对象时会构造一次
this.name = name
}
bark() {}
jump() {}
}
Dog.name // 'Dog'
// 类表达式:可以命名(类的 name 属性取类名),也可以不命名(类的 name 属性取变量名)
let Animal2 = class {
// xxx
}
Animal2.name // 'Animal2'

JS 中的类建立在原型的基础上(通过函数来模拟类,其实类就是构造函数的语法糖),和 ES5 中构造函数类似,但是也有区别,比如类的内部方法是不可被迭代的:

class Dog {
constructor() {}
bark() {}
jump() {}
}
Object.keys(Dog.prototype) // [] 类的内部方法是不可被迭代的

// 类似于
function Dog2(){}
Dog2.prototype = {
constructor() {},
bark() {},
jump() {},
}
Object.keys(Dog2.prototype) // ['constructor', 'bark', 'jump'] function可以被迭代

基于原型给类添加新方法

Object.assign(Dog.prototype, {
eat() {}
})
  • 类声明和类表达式的主体都执行在严格模式下。比如,构造函数,静态方法,原型方法,gettersetter 都在严格模式下执行。
  • 类内部的 this 默认指向类实例,所以如果直接调用原型方法或者静态方法会导致 this 指向运行时的环境,而类内部是严格模式,所以此时的 this 会是 undefined
class Dog {
constructor(name) {
this.name = name
}
bark() {
console.log( `${this.name} is bark.` )
}
static jump() {
console.log( `${this.name} is jump.` )
}
}
let dog = new Dog('大黄')
let { bark } = dog
let { jump } = Dog
bark() // TypeError: Cannot read property 'name' of undefined
jump() // TypeError: Cannot read property 'name' of undefined

方法和关键字

constructor 方法是类的默认方法,通过 new 关键字生成实例的时候,会自动调用;constructor 默认会返回实例对象:

class Point {}
// 一个类必须有 constructor 方法,如果没有显示定义,则会自动添加一个空的,同下
class Point {
constructor() {}
}

通过 getset 关键字拦截某个属性的读写操作:

class Dog {
get age(){
return 1
}
}

static 关键字给类定义静态方法,静态方法不会存在类的原型上,所以不能通过类实例调用,只能通过类名来调用,静态方法和原型方法可以同名:

class Dog {
bark() {}
jump() {
console.log('原型方法')
}
static jump() {
console.log('静态方法')
}
}
Object.getOwnPropertyNames(Dog.prototype) // ['constructor', 'bark', 'jump']
Dog.jump() // '静态方法'
let dog = new Dog()
dog.jump() // '原型方法'

new.target 属性允许你检测函数、构造方法或者类是否是通过 new 运算符被调用的。在通过 new 运算符被初始化的函数或构造方法中,new.target 返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target 的值是 undefined,子类继承父类的时候会返回子类:

class Dog {
constructor() {
console.log(new.target.name)
}
}
function fn(){
if (!new.target) return 'new target is undefined'
console.log('fn is called by new')
}
let dog = new Dog() // 'Dog'
fn() // 'new target is undefined'
new fn() // 'fn is called by new'

类的继承

类可以通过 extends 关键字实现继承,如果子类显示的定义了 constructor 则必须在内部调用 super() 方法,内部的 this 指向当前子类:

class Animal {
constructor(name) {
this.name = name
}
run() {
console.log(`${this.name} is running.`)
}
}
class Dog extends Animal{
constructor(name){
super(name) // 必须调用,就相当于调用了 Animal 的 constructor
this.name = name
}
bark() {
console.log(`${this.name} is barking.`)
}
}
let dog = new Dog('大黄')
dog.run() // '大黄 is running.'

通过 super() 调用父类的构造函数或者通过 super 调用父类的原型方法;另外也可以在子类的静态方法里通过 super 调用父类的静态方法:

// 基于上面的代码改造
class Dog extends Animal{
constructor(name){
super(name) // 调用父类构造函数
this.name = name
}
bark() {
super.run() // 调用父类原型方法
console.log(`${this.name} is barking.`)
}
}
let dog = new Dog()
dog.bark()s
// '大黄 is running.'
// '大黄 is barking.'

子类的 __proto__ 属性,表示构造函数的继承,总是指向父类;子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的prototype属性:

class Animal {}
class Dog extends Animal {}

Dog.__proto__ === Animal // true
Dog.prototype.__proto__ === Animal.prototype // true

子类原型的原型指向父类的原型:

// 基于上面的代码
let animal = new Animal()
let dog = new Dog()
dog.__proto__.__proto__ === animal.__proto__ // true

使用 extends 还可以实现继承原生的构造函数,如下这些构造函数都可以被继承:

String()Number()Boolean()Array()Object()Function()Date()RegExp()Error()

class MyString extends String {
constructor(name){
super(name)
this.name = name
}
welcome() {
return `hello ${this.name}`
}
}
let ms = new MyString('布兰')
ms.welcome() // 'hello 布兰'
ms.length // 2
ms.indexOf('兰') // 1

静态字段

直接通过类来调用,这就称为“静态方法”。

ES 2022 支持静态字段

class MyString  {
constructor(name){
this.name = name
}
// 通过在类中使用 static 关键字,来声明静态方法
static welcome() {
return `hello ${this.name}`
}
hello() {
return `hello ${this.name}`
}
}
let ms = new MyString('布兰')
ms.welcome() // 'hello 布兰'

私有字段

ES 2022 支持私有字段

静态公有字段和静态方法一样只能通过类名调用;私有属性和私有方法只能在类的内部调用,外部调用将报错:

class Dog {
age = 12 // 公有字段
static sex = 'male' // 静态公有字段
#secret = '我是人类的好朋友' // 私有字段
#getSecret() { // 私有方法
return this.#secret
}
}
Dog.sex // 'male'
let dog = new Dog()
dog.#getSecret() // SyntaxError
class SampleClass {
/*
instead of:
constructor() { this.publicID = 42; }
*/
publicID = 42; // public field
/*
instead of:
static get staticPublicField() { return -1 }
*/
static staticPublicField = -1;
// static private field
static #staticPrivateField = 'private';
//private methods
#privateMethod() {}
// static block
static {
// executed when the class is created
}
}

ergonomic brand checks for private fields

Brand checks without exceptions. 📕

class C {
#brand;
#method() {}
get #getter() {}
static isC(obj) {
// in keyword to check
return #brand in obj && #method in obj && #getter in obj;
}
}

公共和私有字段声明是 JavaScript 标准委员会 TC39 提出的实验性功能(第 3 阶段)。浏览器中的支持是有限的,但是可以通过 Babel 等系统构建后使用此功能。

装饰器

JS 的装饰器还在 Stage 3 draft 阶段,但是 ts 的已经可以用了,虽然可能还会再有改动

装饰器的 Polyfill 使用的是 defineProperty 我个人也不是很清楚 Proxy 和 defineProperty 的使用界限,或许会一方代替另一方。当然这都无关代码开发。

使用的时候要搞清楚一点,进行修饰的始终是个方法,用 @ 后面跟的是个方法,因此也有一部分使用了高阶函数,想了解高阶函数,可以查看

类的装饰器

因为没有对应的装饰器环境,推迟更新,等待下次填坑(2022-01-27)

// 一般用法
// 装饰器一般是一个方法,用来对类进行装饰。该方法内的第一个参数为类

const Knowlage = (specc)=>{
specc.prototype.name = '411428'
specc.prototype.speak = function(err){
console.log(this.locate);
}
}
@Knowlage
class Student{
constructor(name){
this.name = name
}
locate="河北省"
speak(){
console.log('老子什么都不输出');
}
}
// 高阶函数用法
const Wisdom = (name, age) => {
console.log(age);
return function (specc) {
specc.prototype.test = '444';
specc.prototype.speak = function (err) {
console.log(this.locate);
}
}
}
@Wisdom('平淡', 66)
class Talent {
constructor(name) {
this.name = name;
}
locate = '河北省';
name;
speak() {
console.log('老子什么都不输出');
}
}

属性的装饰器

装饰器组合

其他的语言已经有装饰器的比较成熟的规范,JS正在进行跟进,哈哈哈哈哈,都拿来吧老子还想学(疯掉.jpg)。

decorator,用于装饰一个类,装饰就在于,你已经有一个非常大的对象了,而你想在其中几个对象中进行修改,但是再创建一个新的对象也不合适,所以就是用装饰器,对其中一部分对象进行修饰(小幅度的改动)。

类的修饰器

function language(value){
return function(target){
target.language = value
}
}
@language('English')
class Country{}
Country.language // English

实例方法修饰器

修饰函数有三个参数

  • target 类的 prototype
  • name 修饰的方法名称
  • escriptor 该方法的修饰器
// 修饰函数有三个参数


JS的修饰器主要依赖于 Object.defineProperty 功能,当解释器遇到了 @decorator 的语法时,就会调用这个修饰器函数对属性(方法)的描述符(descriptor)进行操作,然后将修饰过的 descriptor 重新定义属性

ES5 面向对象

class 是 ES6 的语法糖,可以直接声明类,在没有语法糖之前如何实现呢?

对象的封装方式

普通封装(调用方法返回对象)

function creatPhone(name,prise){
var obj = new Object();
obj.name = name;
obj.prise = prise;
console.log(obj.name);
console.log(obj.prise);
return obj;
}
var xiaomi = creatPhone("小米","996");
var zhongxing = creatPhone("华为","007");

new 封装(通过方法创建对象)

构造函数(使用 new 关键字创建的对象)构造函数一般首字母大写

function Phone(name,prise){
this.name = name;
this.prise = prise;
this.showname=function(){
alert(obj.name);
};
alert(this.prise);
}
var xiaomi = new Phone("小米","996");
xiaomi.showname()

两者区别

  • 普通封装

    (方法)没办法继承,只是通过方法创建了个独立对象

    执行方法,返回值是创建好的对象

  • new 封装

    当前函数中的 this 指向新创建的对象

    自动返回新创建的对象,可以封装函数可以继承

原型对象

原型通常指的是 prototype__proto__ 这两个原型对象。

prototype(显式原型对象):prototype 上的内容会在 new 的时候,作为新对象的属性保留下来。

// 构造函数
function Phone(name, prise) {
this.name = name;
this.prise = prise;
};
// 构造函数的原型对象
Phone.prototype.showName = function () {
alert("选择生产的手机为:" + this.name);
}
Phone.prototype.showWork = function () {
alert("工作时间为" + this.prise);
}
var xiaomi = new Phone("小米", "996");
var huawei = new Phone("华为", "007");
xiaomi.showName();
huawei.showWork();

__proto__(隐式原型对象):__proto__ 上的内容会作为静态方法在构造函数上直接提供调用

给构造函数上添加原型对象 prototype,能为原型对象添加方法,那么构造函数构造出来的对象共享原型上所有的方法。

prototype__proto__

// function 声明一个类,常用的内容
function Puppy(age) {
this.puppyAge = age; // this 指向创建后的对象,认为初始是空的即可
}
Puppy.prototype.say = function(){
console.log('旺旺');
}
Puppy.__proto__.say = function(){
console.log('阿嘎');
}
// 实例化时可以传年龄参数了
const myPuppy = new Puppy(2);
myPuppy.puppyAge // 证明了指向的是构造后的函数
myPuppy.say() // 调用的是 Puppy 使用 prototype 生成的
Puppy.say() // 调用的是 Puppy 使用 __proto__ 生成的

// console.log(new Puppy()==new Puppy()) // false
Puppy.prototype.say === myDog.say // true
myPuppy.__proto__ === Puppy.prototype // true
Puppy.prototype.__proto__ === Object.prototype // true 说明,所有 new 的构造方法 prototype 都是挂载在一个 new 的 Object 上面,如果要实现继承,只要更改 Puppy.prototype.__proto__ 让它指向父类的 prototype 即可

constructor

一个保留关键字,用来指向 prototype 或者 __proto__ 链接的对象

function Puppy(age) {
this.puppyAge = age; // this 指向创建后的对象,认为初始是空的即可
}
Puppy.prototype.say = function(){
console.log('旺旺');
}
Puppy.__proto__.say = function(){
console.log('阿嘎');
}
const myPuppy = new Puppy(2);
myPuppy.__proto__.constructor.__proto__.say()
myPuppy.say()

如果修改构造函数的 constructor 并不会修改构造函数

或者直接理解,prototype.constructor 只是一个指针,类似于 this 改变 this 只是改变 this 的指向,而不是 this 指向内容的值

function Puppy(age) {
this.puppyAge = age;
}
// 如果修改 Puppy.prototype.constructor 只会修改 constructor 指针的指向
Puppy.prototype.constructor = function myConstructor(age) {
this.puppyAge = age + 1;
console.log(this.puppyAge)
}
const myPuppy = new Puppy(5)

myPuppy2.constructor(999)

静态方法的实现

比较直接的实现

function Puppy(age){
this.puppyAge = age
}
// name 为保留字,不能设置 Puppy.name
Puppy.cuier = '高'
console.log(Puppy.cuier)

Puppy.__proto__ 中修改,会连带修改 Puppy 上的内容

function Puppy(age) {
this.puppyAge = age
}
Puppy.cuier = '临高'
Puppy.__proto__.cuier = '启明'
console.log(Puppy.cuier)
console.log(Puppy.__proto__.cuier)
console.log(Puppy.__proto__.cuier === Puppy.cuier)

Puppy.__proto__.fake = '虚假'
console.log(Puppy.fake)
console.log(Puppy.__proto__.fake)
console.log(Puppy.__proto__.fake === Puppy.fake)

Puppy.truth = '真实'
console.log(Puppy.truth)
console.log(Puppy.__proto__.truth)

继承的实现

继承:子类继承父类,会让子类拥有所有父类对象上面的内容。

子类上可以添加内容,或者是覆盖父类上面的内容

如何实现继承,要同时实现原型的继承和静态方法的继承。

原型的继承,让 __proto__ 指向父类原型的 prototype 就可以了

function Parent() {}
function Child() {}

Child.prototype.__proto__ = Parent.prototype

但是这样做,只是让 Child 能够访问到 Parentprototype

所以,必须使用

function Parent() {
this.parentAge = 50
}
function Child() {}

// Child.prototype.__proto__ = Parent.prototype;// 如果通过该方法进行继承,只能让 Child 访问,Child 的子对象无法访问
Child.prototype.__proto__ = new Parent() // 通过让 Child 的原型指向 Parent 来实现继承

const obj = new Child()
console.log(obj.parentAge)

或者使用

function Parent() {
this.parentAge = 50
}
function Child() {}

Child.prototype = new Parent()
Child.prototype.constructor = Child
// 如果 Child.prototype 指向了 Parent,Child.prototype.constrcutor 也会跟着指向 Parent,而不是 Child

const obj = new Child()
console.log(Parent.prototype.constructor)

静态方法的继承

如果将父对象的原型对象赋值给子对象,会导致两个对象公用同一个原型对象

解决办法,用 for in 循环进行原型对象的赋值

function Dog({name,sex,master}){
this.name = name;
this.sex=sex;
this.master =master;
}
Dog.prototype.file = function(){
alert(`这只狗是${this.master}先生家的一条狗,是一条${this.sex}${this.name}`);
};
function TinyDog({name,sex,master,weight}){
Dog.call(this,{
name:name,
sex:sex,
master:master
});
this.weight = weight;
};
// 使用for循环进行继承
for(var attr in Dog.prototype){
TinyDog.prototype[attr] = Dog.prototype[attr];
}
var bomei = new TinyDog({
name:"博美犬",
sex:"male",
master:"老刘",
weight:0.78
});
bomei.file();
var alasika = new TinyDog({
name:"阿拉斯加",
sex:"female",
master:"老赵",
weight:1.8
})
// 用构造函数构造的对象,有一个__proto__,指向构造出这个对象的构造函数原型
alert(bomei.__proto__==Dog.prototype);
// instanceof 用于判断某一个对象是否是这个构造函数构造的
alert(bomei instanceof Dog);
  • 继承侧重的是父一级继承的构造函数和方法
  • 多态侧重的是子一级可以重写构造函数和方法

其它继承方式

通过 for...in 循环遍历继承

for(var funcName in person.prototype){
Worker.prototype[funcName] = Person.prototype[funcName];
}

Object.create()

Worker.prototype = Object.create(Person.prototype);
// 使用 Worker 继承 Person

调用函数构造继承

Worker.prototype.__proto__ = new Person();

总结

img

prototype 本身也是对象,所以他也有 __proto__,指向了他父级的 prototype__proto__prototype 的这种链式指向构成了 JS 的原型链。原型链的最终指向是Object的原型。Object上面原型链是null,即Object.prototype.__proto__ === null

prototype.constructor指向的是构造函数,也就是类函数本身。改变这个指针并不能改变构造函数。

为了让实例化出来的对象能够访问到prototype上的属性和方法,实例对象的__proto__指向了类的prototype。所以prototype是函数的属性,不是对象的。对象拥有的是__proto__,是用来查找prototype的。

参考文章

作者文章名称
蒋鹏飞轻松理解JS中的面向对象,顺便搞懂prototype__proto__
babel官方文档https://babeljs.io/docs/en/babel-plugin-proposal-numeric-separator
四月白绵羊https://www.jianshu.com/p/43d88a4180f8
nu11https://juejin.cn/post/6844904100144889864
Hemanth HMES2022 Features!

相关文章

知识点链接
装饰器https://www.jianshu.com/p/afef44d449bd
装饰器https://juejin.cn/post/6844903876605280269
装饰器https://juejin.cn/post/6844904100144889864
装饰器https://juejin.cn/post/6999451760934780941